import os
from typing import List, Optional, Union
from pathlib import Path
import smtplib
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
from email.mime.base import MIMEBase
from email import encoders

from exchangelib import (
    Credentials,
    Configuration,
    Account,
    DELEGATE,
    Message,
    Mailbox,
    HTMLBody,
    FileAttachment,
    Q,
)
from exchangelib.errors import (
    AutoDiscoverFailed,
    TransportError,
    UnauthorizedError,
)

class OutlookMailClient:
    """
    Outlook 邮件客户端封装，基于 exchangelib。

    提供：连接账户、发送邮件、筛选读取、下载附件等方法。
    """
    def __init__(
        self,
        email_address: str,
        password: str,
        autodiscover: bool = True,
    ):
        self.email_address = email_address
        self.password = password
        self.autodiscover = autodiscover
        self.account = self._connect()

    def _connect(self) -> Account:
        """
        建立到 Exchange/Outlook 的连接。

        Returns:
            Account: 已认证的账户对象。
        """
        creds = Credentials(username=self.email_address, password=self.password)
        if self.autodiscover:
            try:
                return Account(
                    primary_smtp_address=self.email_address,
                    credentials=creds,
                    autodiscover=True,
                    access_type=DELEGATE,
                )
            except (AutoDiscoverFailed, UnauthorizedError, TransportError):
                # 自动发现失败或凭证不被接受时，回退到手动配置
                pass

        cfg = Configuration(
            server="outlook.office365.com",
            credentials=creds,
        )
        return Account(
            primary_smtp_address=self.email_address,
            credentials=creds,
            autodiscover=False,
            config=cfg,
            access_type=DELEGATE,
        )

    def send_email(
        self,
        to: Union[str, List[str]],
        subject: str,
        body_text: Optional[str] = None,
        body_html: Optional[str] = None,
        cc: Optional[List[str]] = None,
        bcc: Optional[List[str]] = None,
        attachments: Optional[List[Union[str, bytes]]] = None,
    ) -> None:
        """
        发送邮件，支持文本/HTML 正文与附件。

        Args:
            to: 收件人邮箱或邮箱列表。
            subject: 邮件主题。
            body_text: 纯文本正文。
            body_html: HTML 正文（优先使用）。
            cc: 抄送列表。
            bcc: 密送列表。
            attachments: 附件路径或字节内容列表。
        """
        if isinstance(to, str):
            to_list = [to]
        else:
            to_list = to

        msg = Message(
            account=self.account,
            subject=subject,
            body=HTMLBody(body_html) if body_html else (body_text or ""),
            to_recipients=[Mailbox(email_address=a) for a in to_list],
            cc_recipients=[Mailbox(email_address=a) for a in (cc or [])],
            bcc_recipients=[Mailbox(email_address=a) for a in (bcc or [])],
        )

        if attachments:
            for att in attachments:
                if isinstance(att, bytes):
                    msg.attach(FileAttachment(name="attachment.bin", content=att))
                elif isinstance(att, str):
                    p = Path(att)
                    if p.exists() and p.is_file():
                        msg.attach(FileAttachment(name=p.name, content=p.read_bytes()))
                else:
                    raise ValueError("Unsupported attachment type")

        try:
            msg.send_and_save()
        except UnauthorizedError as e:
            # 当 EWS 被禁用基本认证时，回退到 SMTP 发送
            self._smtp_send_email(
                to=to_list,
                subject=subject,
                body_text=body_text,
                body_html=body_html,
                cc=cc,
                bcc=bcc,
                attachments=attachments,
            )
            return
        except TransportError as e:
            raise RuntimeError("网络或服务异常：请检查网络连通性或稍后再试") from e

    def list_inbox(
        self,
        unread_only: bool = False,
        subject_contains: Optional[str] = None,
        from_address: Optional[str] = None,
        limit: int = 50,
    ):
        """
        列出收件箱邮件，可按未读、主题包含、发件人过滤。

        Args:
            unread_only: 是否仅未读。
            subject_contains: 主题包含关键词。
            from_address: 发件人邮箱包含。
            limit: 最大返回数量。

        Returns:
            QuerySet: 邮件查询集合（已按接收时间倒序）。
        """
        qs = self.account.inbox.all()
        if unread_only:
            qs = qs.filter(is_read=False)
        if subject_contains:
            qs = qs.filter(subject__contains=subject_contains)
        if from_address:
            qs = qs.filter(sender__email_address__icontains=from_address)
        return qs.order_by("-datetime_received")[:limit]

    def download_attachments(
        self,
        save_dir: Union[str, Path],
        unread_only: bool = False,
        subject_contains: Optional[str] = None,
        from_address: Optional[str] = None,
        limit: int = 50,
    ) -> List[Path]:
        """
        下载筛选后的邮件附件到指定目录。

        Args:
            save_dir: 保存目录路径。
            unread_only: 是否仅处理未读邮件。
            subject_contains: 主题关键词筛选。
            from_address: 发件人筛选。
            limit: 处理的最大邮件数。

        Returns:
            List[Path]: 已保存附件的文件路径列表。
        """
        save_path = Path(save_dir)
        save_path.mkdir(parents=True, exist_ok=True)

        items = self.list_inbox(
            unread_only=unread_only,
            subject_contains=subject_contains,
            from_address=from_address,
            limit=limit,
        )

        saved_files: List[Path] = []
        for item in items:
            for att in item.attachments:
                if isinstance(att, FileAttachment):
                    target = save_path / att.name
                    try:
                        with open(target, "wb") as f:
                            f.write(att.content)
                        saved_files.append(target)
                    except OSError as e:
                        raise RuntimeError(f"附件保存失败：{att.name}") from e
        return saved_files

    def _smtp_send_email(
        self,
        to: List[str],
        subject: str,
        body_text: Optional[str] = None,
        body_html: Optional[str] = None,
        cc: Optional[List[str]] = None,
        bcc: Optional[List[str]] = None,
        attachments: Optional[List[Union[str, bytes]]] = None,
    ) -> None:
        """
        使用 SMTP 发送邮件（适用于应用密码/禁用 EWS 基本认证场景）。

        Args:
            to: 收件人列表。
            subject: 邮件主题。
            body_text: 纯文本正文。
            body_html: HTML 正文（优先）。
            cc: 抄送列表。
            bcc: 密送列表。
            attachments: 附件路径或字节内容列表。
        """
        message = MIMEMultipart()
        message["From"] = self.email_address
        message["To"] = ", ".join(to)
        if cc:
            message["Cc"] = ", ".join(cc)
        message["Subject"] = subject

        # 正文：优先 HTML
        if body_html:
            message.attach(MIMEText(body_html, "html", "utf-8"))
        elif body_text:
            message.attach(MIMEText(body_text, "plain", "utf-8"))
        else:
            message.attach(MIMEText("", "plain", "utf-8"))

        # 附件
        if attachments:
            for att in attachments:
                if isinstance(att, bytes):
                    part = MIMEBase("application", "octet-stream")
                    part.set_payload(att)
                    encoders.encode_base64(part)
                    part.add_header("Content-Disposition", f"attachment; filename=attachment.bin")
                    message.attach(part)
                elif isinstance(att, str):
                    p = Path(att)
                    if p.exists() and p.is_file():
                        data = p.read_bytes()
                        part = MIMEBase("application", "octet-stream")
                        part.set_payload(data)
                        encoders.encode_base64(part)
                        part.add_header("Content-Disposition", f"attachment; filename={p.name}")
                        message.attach(part)

        # 汇总收件人
        recipients = list(to)
        if cc:
            recipients.extend(cc)
        if bcc:
            recipients.extend(bcc)

        # 连接并发送（尝试多个端点以提升兼容性）
        endpoints = [
            ("smtp.office365.com", 587),
            ("smtp-mail.outlook.com", 587),
        ]
        last_err: Optional[Exception] = None
        for host, port in endpoints:
            try:
                with smtplib.SMTP(host, port, timeout=30) as server:
                    server.ehlo()
                    server.starttls()
                    server.ehlo()
                    server.login(self.email_address, self.password)
                    server.sendmail(self.email_address, recipients, message.as_string())
                return
            except Exception as e:
                last_err = e
                continue
        if last_err:
            raise RuntimeError(f"SMTP 发送失败：{last_err}")

def load_env_file(env_path: Union[str, Path] = ".env") -> None:
    """
    从同目录 .env 文件加载环境变量（格式 KEY=VALUE）。

    - 忽略空行与以 `#` 开头的注释。
    - 优先保留已存在的系统环境变量，不强制覆盖。

    Args:
        env_path: .env 文件路径，默认同目录下的 ".env"。
    """
    p = Path(env_path)
    if not p.is_absolute():
        p = Path(__file__).parent / p
    if not p.exists():
        return

    try:
        # 使用 utf-8-sig 以自动去除可能存在的 BOM
        for raw in p.read_text(encoding="utf-8-sig").splitlines():
            line = raw.strip()
            if not line or line.startswith("#"):
                continue
            if "=" not in line:
                continue
            key, val = line.split("=", 1)
            key = key.strip()
            val = val.strip().strip('"').strip("'")
            if key:
                os.environ.setdefault(key, val)
    except Exception:
        # 读取失败时静默跳过，保持回退到系统环境变量
        pass


def _get_env(name: str) -> str:
    """
    获取环境变量值，若缺失则抛出异常。

    Args:
        name: 环境变量名。

    Returns:
        str: 环境变量的值。
    """
    v = os.getenv(name)
    if not v:
        raise RuntimeError(f"缺少环境变量：{name}")
    return v


if __name__ == "__main__":
    # 先尝试从同目录 .env 文件加载，再读取环境变量（回退）
    load_env_file(".env")
    # 从环境变量读取账号与密码，避免硬编码
    email = _get_env("OUTLOOK_EMAIL")
    password = _get_env("OUTLOOK_PASSWORD")

    client = OutlookMailClient(email_address=email, password=password, autodiscover=True)

    # 发送示例（带附件）
    try:
        client.send_email(
            to="recipient@example.com",
            subject="测试主题 - exchangelib",
            body_text="这是一封测试邮件（文本正文）。",
            body_html="<p>这是一封测试邮件（<b>HTML正文</b>）。</p>",
            attachments=["/path/to/optional.pdf"],  # 可去掉或替换
        )
        print("邮件已发送")
    except Exception as e:
        print(f"发送失败：{e}")

    # 接收/筛选/下载附件示例
    try:
        emails = client.list_inbox(unread_only=True, subject_contains="订单", limit=10)
        for it in emails:
            print(f"[{it.datetime_received}] {it.sender.email_address} - {it.subject}")

        saved = client.download_attachments(
            save_dir="./downloads",
            unread_only=True,
            subject_contains="订单",
            limit=10,
        )
        for p in saved:
            print(f"已保存附件：{p}")
    except Exception as e:
        print(f"接收或下载失败：{e}")